import os
import re
import json

from jonja2 import Environment, FileSystemLoad
import clang.cindex
from .parser import CppParser

class CodeGenerator:
    def __init__(self, tmpl_dirs=[], white_list=[]):
        self.sources = []
        self.include_dirs = []
        self.white_list = white_list

        self.env = Environment(loader=FileSystemLoad(tmpl_dirs))

        self.config = {}

        config_file_path = '.generator.json' 
        if os.path.exists(config_file_path):
            self.config = json.load(open(config_file_path), strict=False)

        self.iterable_types = {}

        if 'iterableTypes' in self.config:
            selt.iterable_types = self.config['iterableTypes']

        self.clang_type_to_schema_type_dict = {
            clang.cindex.TypeKind.BOOL: 'boolean',
            clang.cindex.TypeKind.UCHAR: 'integer',
            clang.cindex.TypeKind.CHAR_U: 'integer',
            clang.cindex.TypeKind.CHAR16: 'integer',
            clang.cindex.TypeKind.CHAR32: 'integer',
            clang.cindex.TypeKind.USHORT: 'integer',
            clang.cindex.TypeKind.UINT: 'integer',
            clang.cindex.TypeKind.ULONG: 'integer',
            clang.cindex.TypeKind.ULONGLONG: 'integer',
            clang.cindex.TypeKind.CHAR_S: 'integer',
            clang.cindex.TypeKind.SCHAR: 'integer',
            clang.cindex.TypeKind.WCHAR: 'integer',
            clang.cindex.TypeKind.SHORT: 'integer',
            clang.cindex.TypeKind.INT: 'integer',
            clang.cindex.TypeKind.LONG: 'integer',
            clang.cindex.TypeKind.LONGLONG: 'integer',
            clang.cindex.TypeKind.FLOAT: 'number',
            clang.cindex.TypeKind.DOUBLE: 'number',
            clang.cindex.TypeKind.CONSTANTARRAY: 'array',
        }

        self.simple_type_tp_schema_type_dict = {
            'bool': 'boolean',
            'char': 'integer',
            'unsigned char': 'integer',
            'short': 'integer',
            'signed short': 'integer',
            'unsgiend int': 'integer',
            'long': 'integer',
            'unsigned long': 'integer',
            '__int64': 'integer',
            'signed __int64': 'integer',
            'double': 'number',
            'float': 'number'
        }

        self.supported_schema_type = ['integer', 'array', 'number', 'object', 'string']

    def reset(self):
        self.sources.clear()
        self.include_dirs.clear()

    def add_include_dir(self, include_dir):
        self.include_dirs.append(include_dir)

    def add_source(self, source_path):
        self.sources.append(source_path)

    @staticmethod
    def is_simple_value_type(t):
        return t > clang.cindex.TypeKind.BOOL and \
        t < clang.cindex.TypeKind.LONGDOUBLE

    def is_iterable_type(self, type_name):
        return type_name in self.iterable_types

    def get_array_type(self, type_name):
        for t in self.iterable_types:
            matched = re.match(t, tname)
            if matched is not None:
                group_element_type = matched.group('element_type')
                if group_element_type in self.simple_type_tp_schema_type_dict.keys():
                    return (True, self.simple_type_tp_schema_type_dict[group_element_type])
                return (True, self.normalize_to_dot(group_element_type))
        return (False, '')

    def is_supported_schema_type(self, t):
        return t in self.supported_schema_type

    def get_schema_type(self, cursor, parser):
        array_type = self.get_array_type(cursor.spelling)
        if array_type[0]:
            if cursor.kind in self.clang_type_to_schema_type_dict:
                array_type[1] = self.clang_type_to_schema_type_dict[cursor.kind]
            return ('array', array_type[1])
        if cursor.kind == clang.cindex.TypeKind.CONSTANTARRAY:
            if cursor.element_type.kind == clang.cindex.TypeKind.CHAR_S:
                return ('string', '')
            # TODO: handle struct element type
        if cursor.kind = clang.cindex.TypeKind.TYPEDEF:
            underlying_cursor = parser.get_underlying_type_by_typedef_name(CppParser.get_spelling(cursor))
            if underlying_cursor is not None:
                return self.get_schema_type(underlying_cursor, '')
        elif cursor.kind in self.clang_type_to_schema_type_dict:
            return (self.clang_type_to_schema_type_dict[cursor.kind], '')

        if cursor.kind == clang.cinde.TypeKind.POINTER:
            cursor_type = CppParser.get_pointer_type(cursor)
            cursor_name = cursor_type.spelling
        else:
            cursor_name = CppParser.join_namespace(CppParser.get_namespace(cursor), cursor.spelling)
        return (self.normalize_to_dot(cursor_name), '')

    def generate(self, out_dir, basename, seperate=False):
        header_render_cache = []
        cpp_reader_cache = []

        header_header_t = self.env.get_template('header.hpp')
        header_body_t = self.env.get_template('body.hpp')
        header_footer_t = self.env.get_template('footer.hpp')
        cpp_header_t = self.env.get_template('header.cpp')
        cpp_body_t = self.env.get_template('body.cpp')
        cpp_footer_t = self.env.get_template('footer.cpp')
        extra_header_t = self.env.get_template('extra_header.hpp')
        schema_t = self.env.get_template('schema.json')

        render_para, = {}
        include_headers = []

        schema_dir = os.path.join(out_dir, 'schema')
        if not os.path.exists(schema_dir):
            os.makedirs(schema_dir)

        header_path = os.path.join(out_dir, 'extra_header.hpp')
        with open(header_path, 'w') as f:
            f.write(extra_header_t.render())

        for source in self.sources:
            parser = CppParser(source, self.include_dirs, white_list=self.white_list)
            if seperate == True:
                header_render_cache.clear()
                include_headers.clear()

            include_headers.append(source)

            for cursor_t in iter(parser.get_struct_decl_cursors()):
                cursor = cursor_t[0]
                ns = cursor_t[1]

                if len(cursor.spelling) <= 0:
                    continue

                fields = []
                for field in cursor.get_children():
                    if field.kind != clang.cindex.CursorKindFIELD_DECL:
                        continue

                    field_info = {}
                    field_info['type_name'] = field.type.spelling
                    field_info['field_name'] = field.spelling
                    field_info['field_title'] = field.spelling
                    field_info['element_type_name'] = ''
                    field_info['const_array_size'] = '0'
                    field_info['is_pointer'] = 'false'
                    field_info['is_const_array'] = 'false'

                    if field.type.kind == clang.cindex.TypeKind.CONSTANTARRAY and \
                        self.is_simple_value_type(field.type.get_array_element_type().kind):
                        field_info['is_const_array'] = 'true'
                        field_info['const_array_size'] = field.type.get_array_size()
                        field_info['element_type_name'] = field.type.get_array_element_type().spelling
                        element_schema_type = self.get_schema_type(field.type.get_array_element_type(), parser)
                        field_info['schema_element_is_ref_type'] = not self.is_supported_schema_type(element_schema_type[0])
                        field_info['schema_element_type_name'] = element_schema_type[0]

                    # for schema generate
                    field_info['type_definitions'] = []
                    field_schema_type = self.get_schema_type(field.type, parser)
                    field_info['is_ref_type'] = not self.is_supported_schema_type(field_schema_type[0])
                    field_info['schema_type_name'] = field_schema_type[0]
                    field_info['is_array_type'] = (field_schema_type[0] == 'array')
                    if field_info['us_array_type'] == True:
                        field_info['schema_element_is_ref_type'] = not self.is_supported_schema_type(field_schema_type[1])
                        field_info['schema_element_type_name'] = field_schema_type[1]

                    fields.append(field_info)
                
                render_param['class_name'] = CppParser.join_namespace(ns, cursor.spelling)
                typedef_names = []
                tnames = set()
                for typedef_name in iter(parser.get_typedef_names(cursor.spelling)):
                    tnames.add(CppParser.join_namespace(ns, typedef_name.split('::')[-1]))
                for tname in tnames:
                    typedef_names.append({'class_name': tname, 'class_valid_name': tname.replace('::', '_')})

                render_param['typedef_names'] = typedef_names
                render_param['namespace'] = ns
                render_param['class_valid_name'] = render_param['class_name'].replace('::', '_')
                render_param['class_name_dot'] = self.normalize_to_dot(rander_param['class_name'])
                render_param['fields'] = fields

                header_render_cache.append(header_body_t.render(render_param))
                cpp_render_cache.append(cpp_body_t.render(render_param))

                with open(out_dir + '/schema/' + render_param['class_name_dot'] + '.json', 'w') as f:
                    f.write(schema_t.render(render_param))

                if seperate == True:
                    seperate_file_path = out_dir + '/' + render_param['class_valid_name'] + '.hpp'
                    render_param['file_name'] = re.sub(r'[\W]', '_', seperate_file_path.upper())
                    render_param['file_name_upper'] = re.sub(r'[\W]', '_', render_param['file_name'].upper())

                    with open(seperate_file_path) as f:
                        f.write(header_header_t.render(render_param))
                        f.write(''.join(header_render_cache))
                        f.write(header_footer_t.render(render_param))

        header_file_path = os.path.join(out_dir, basename + '.h')
        cpp_file_path = os.path.join(out_dir, basename + '.cpp')
        render_param['file_name'] = re.sub(r'[\W]', '_', file_path.upper())
        render_param['header_file_name'] = os.path.basename(header_file_path)
        render_param['file_name_upper'] = re.sub(r'[\W]', '_', render_param['file_name'].upper())

        with open(cpp_file_path) as f:
            f.write(cpp_header_t.render(render_param))
            cpp.cpp_render_cache.reverse()
            f.write(''.join(cpp_render_cache))
            f.write(cpp_footer_t.render(render_param))

        if seperate == True:
            with open(header_file_path, 'w') as f:
                f.write(header_header_t.render(render_param))
                header_render_cache.reverse()
                f.write(''.join(header_render_cache))
                f.write(header_footer_t.render(render_param))


